<!--
@license
Copyright 2017 GIVe Authors
*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../bower_components/iron-resizable-behavior/iron-resizable-behavior.html">
<link rel="import" href="../give-tree/give-tree.html">
<link rel="import" href="../chrom-region/chrom-region.html">
<link rel="import" href="track-object.html">
<link rel="import" href="../ref-object/ref-object.html">
<script>
  var GIVe = (function (give) {
    give._trackCounter = 0

    /**
     * Basic track DOM behaviors, including the svg drawing functions
     *
     * @polymerBehavior TrackDOMBehavior
     */
    give.TrackDOMBehaviorImpl = {

      properties: {
        /**
         * The underlying track object for metadata, settings,
         * actual data and communication with the server
         * @type {GIVE.TrackObject}
         */
        parent: {
          type: Object,
          readOnly: true
        },

        /**
         * The CSS class for the main SVG element
         * @type {string}
         */
        trackMainDOMClass: {
          type: String,
          readOnly: true
        },

        /**
         * Text size of the track, will also affect line sizes.
         * This serves as the standard for the other properties.
         * @type {number}
         */
        textSize: {
          type: Number,
          value: 12
        },

        /**
         * Proportion of full line height (multiplies of `this.textSize`).
         * @type {number}
         */
        fullHeightRatio: {
          type: Number,
          value: 1    // em
        },

        /**
         * Proportion of half height (for bed files, for example). Multiplies
         * of `this.textSize`.
         * @type {number}
         */
        halfHeightRatio: {
          type: Number,
          value: 0.6    // this is the proportion to fullHeight
        },

        /**
         * This is the size of the gap between lines (in a BED file, for example).
         * Multiplies of `this.textSize`
         * @type {number}
         */
        lineGapRatio: {
          type: Number,
          value: 0.1
        },

        /**
         * Interval for update drawing, needs to be a small value for coordinates
         * @type {number}
         */
        _drawDebounceInt: {
          type: Number,
          value: 100
        },

        /**
         * Interval for getting data into cache. This is longer than draw debouncing
         * @type {number}
         */
        _cacheDebounceInt: {  //
          type: Number,
          value: 300
        },

        colorSet: {
          type: Array,
          value: function () {
            return [0x3F51B5, 0x2196F3, 0x009688,
              0x4CAF50, 0xCDDC39, 0xFFC107,
              0x795548, 0xF44336, 0x9C27B0 ]    // put color set here
          }
        },

        /**
         * Flag to indicate whether this track has a dynamic height (from its contents).
         * @type {boolean}
         */
        dynamicHeight: {  // use true if the height of the track is dynamic
          type: Boolean,
          value: true,
          readOnly: true
        },

        /**
         * The actual height of the track in pixels.
         * This will be useful if `this.dynamicHeight === true`
         * @type {number}
         */
        trackHeight: {
          type: Number,
          readOnly: true
        },

        /**
         * The flag indicating whether this trackDOM is ready
         * @type {boolean}
         */
        isReady: {
          type: Boolean,
          value: false,
          readOnly: true
        },

        pin: {
          type: String,
          value: ''
        }

  //      initialized: {
  //        type: Boolean,
  //        value: false,
  //        readOnly: true,
  //      },
      },

      // lifecycle events

      created: function () {
        this.ARROW_HEIGHT_PROP = 0.5    // percentage of arrow (to height of gene)
        this.ARROW_MAX_HEIGHT_PROP = 0.9    // max percentage of arrow
        this.ARROW_MIN_HEIGHT = 4      // minimum height required for arrow
        this.ARROW_RECT_MIN_WIDTH = 1.5  // minimum rectangle width to draw arrow
        this.ARROW_GAP_WIDTH = 1.5      // gap width for arrow
        this.ARROW_FLANK_WIDTH = 0.5    // flank width for arrow

        this.MIN_TOTAL_WIDTH = 100

        this.TEXT_MARGIN_GAP = 5    // flank width for arrow

        this.Y_HIDDEN = -30      // value to hide stuff in svg (to calculate size)

        this.MAX_SPACE_PER_BASE = 1.5
        // maximum space for every base (unit in textSize, cannot zoom further)

        this.svgNS = 'http://www.w3.org/2000/svg'

        // the following names are used in debouncing
        this.updateJobName = 'UPDATE'
        this.cacheUpdateJobName = 'UPDATECACHE'

        this.callbackAfterDraw = null

        this.narrowMode = false
        this.labelMarginRight = 0
      },

      factoryImpl: function (track, properties) {
        // request URL is the target to get data from
        // width is the width given for the track (including text margin), units in px
        // type is reserved

        properties = properties || {}

        this.x = properties.x || 0
        this.y = properties.y || 0
        try {
          this.setHeight(properties.height)
        } catch (e) {
          this.setHeight(100)
        }

        this._setParent(track)
        // this._setTrackType(track.getTypeTrunk())
        this.id = this.parent.getTypeTrunk() + '_' + give._trackCounter++

        // this is the type of visibility values that are allowed in a particular track
        this.allowedVisibilities = [
          give.TrackObject.StatusEnum.VIS_NONE,
          give.TrackObject.StatusEnum.VIS_DENSE,
          give.TrackObject.StatusEnum.VIS_NOTEXT,
          give.TrackObject.StatusEnum.VIS_COLLAPSED,
          give.TrackObject.StatusEnum.VIS_PACK,
          give.TrackObject.StatusEnum.VIS_FULL
        ]
        this.defaultVisibility = give.TrackObject.StatusEnum.VIS_FULL

        this.activeVisibility = this.getTrackSetting('visibility') || this.defaultVisibility

        this.textMargin = this.textMargin || properties.textMargin || 0

        this.windowIndex = this.windowIndex || properties.windowIndex || 0

        this.cacheRegionSpan = give.TrackDOMBehaviorImpl.CacheRangeSpanProportion

        this.windowWidth = properties.width -
                  (properties.textMargin ? properties.textMargin + this.TEXT_MARGIN_GAP : 0)
        this.totalWidth = properties.width

        if (this.windowWidth < this.MIN_TOTAL_WIDTH) {
          this.switchNarrowMode(true)
        }

        if (properties.pin) {
          this.pin = properties.pin
        }

        give._verboseConsole(this.id + ' created. Calling trackImpl().', give.VERBOSE_DEBUG)
        // call "constructors" of child classes
        if (this.trackImpl) {
          this.trackImpl(track, properties)
        }
      },

      getTrackSetting: function (key) {
        return this.parent.getSetting(key)
      },

      setTrackSetting: function (key, value) {
        return this.parent.setSetting(key, value)
      },

      /**
       * getTrackSvg - Get the main SVG element for track display
       *   This will be called after the trackDOM is initialized
       *   (should be later than "attached")
       *
       * @returns {DOMelement}  SVG DOM element
       */
      getTrackSvg: function () {
        // this will be called after the trackDOM is initialized (should be later than "attached")

        if (!this.trackSvg) {
          this.trackSvg = document.createElementNS(this.svgNS, 'svg')
          this.trackSvg.setAttribute('id', this.parent.getCleanID() + '_trackSvg')
          this.trackSvg.setAttribute('class', 'trackSvg')

          // if textMargin is specified, create a svg element for text labels

          if (this.textMargin) {
            this.initTextSvgAndHolder()
          }

          // create dom elements (svg)
          this.mainSvg = document.createElementNS(this.svgNS, 'svg')
          this.mainSvg.setAttribute('id', this.parent.getCleanID() + '_mainSvg')
          if (this.trackMainDOMClass) {
            this.mainSvg.setAttribute('class', this.trackMainDOMClass)
          }
          Polymer.dom(this.trackSvg).appendChild(this.mainSvg)

    //      this._setInitialized(true);
    //      this.fire('track-initialized', {ID: this.parent.getID()});
          this.initSvgHolder(this.mainSvg)

          if (typeof this.initSvgComponents === 'function') {
            this.initSvgComponents()
          }

          this.setSvgSizeLocation()
        }
        give._verboseConsole(this.id + '.getTrackSvg() called.', give.VERBOSE_DEBUG)
        return this.trackSvg
      },

      /**
       * setSvgSizeLocation - Set the size and location (mainly Y value) of
       *   main track SVG element.
       */
      setSvgSizeLocation: function () {
        if (this.trackSvg) {
          this.trackSvg.setAttributeNS(null, 'x', this.x)
          this.trackSvg.setAttributeNS(null, 'y', this.y)
          this.trackSvg.setAttributeNS(null, 'width', this.totalWidth)
          // notice that the heights are placeholder and will change
          this.trackSvg.setAttributeNS(null, 'height', this.trackHeight)
          this.trackSvg.setAttribute('viewBox', '0 0 ' +
            this.totalWidth + ' ' + this.trackHeight)
          if (this.textSvg) {
            this.textSvg.setAttributeNS(null, 'x', 0)
            this.textSvg.setAttributeNS(null, 'y', 0)
            this.textSvg.setAttributeNS(null, 'width', this.textMargin)

            // notice that the heights are placeholder and will change
            this.textSvg.setAttributeNS(null, 'height', this.trackHeight)
            this.textSvg.setAttribute('viewBox', '0 0 ' +
              this.textMargin + ' ' + this.trackHeight)
          }
          this.mainSvg.setAttributeNS(null, 'x', (this.narrowMode ? 0 : this.textMargin + this.TEXT_MARGIN_GAP))
          this.mainSvg.setAttributeNS(null, 'y', 0)
          this.mainSvg.setAttributeNS(null, 'width', this.windowWidth)

          // notice that the heights are placeholder and will change
          this.mainSvg.setAttributeNS(null, 'height', this.trackHeight)
          this.mainSvg.setAttribute('viewBox', '0 0 ' +
            this.windowWidth + ' ' + this.trackHeight)
          if (typeof this.setSvgComponentsSizeLocation === 'function') {
            this.setSvgComponentsSizeLocation()
          }
        }
      },

      /**
       * getHeight - get the height of the track SVG
       *
       * @returns {number}  height value
       */
      getHeight: function () {
        return this.trackHeight
      },

      /**
       * getMainSvg - get the mainSvg element (the SVG with most graphing)
       *
       * @returns {DOMelement}  mainSvg element
       */
      getMainSvg: function () {
        return this.mainSvg
      },

      /**
       * getTextSvg - get the textSvg element (the SVG with side text annotations)
       *
       * @returns {DOMelement}  textSvg element
       */
      getTextSvg: function () {
        return this.textSvg
      },

      /**
       * initTextSvgAndHolder - Initialize the text SVG element and its content
       *   holder.
       */
      initTextSvgAndHolder: function () {
        this.textSvg = document.createElementNS(this.svgNS, 'svg')
        this.textSvg.setAttribute('id', this.parent.getCleanID() + '_textSvg')
        Polymer.dom(this.trackSvg).appendChild(this.textSvg)
        this.initSvgHolder(this.textSvg)
      },

      /**
       * initSvgHolder - Initialize the content holder for an SVG element.
       *
       * @param {DOMelement} svgElem - the SVG element to put the content
       *    holder in.
       */
      initSvgHolder: function (svgElem) {
        // notice that `svgElem` should be there
        svgElem.holder = document.createElementNS(this.svgNS, 'g')
        Polymer.dom(svgElem).appendChild(svgElem.holder)
      },

      /**
       * initSvgReceiver - Add a gesture listener (rectangle) to the SVG element.
       *   Notice that text holder and main holder have their listeners separately.
       *   Also the actual listener is attached to svg elements, not the rectangles
       *
       * @param  {DOMelement} svgToDraw The SVG element to attach listeners to
       */
      initSvgReceiver: function (svgToDraw) {
        if (!svgToDraw.gestureReceiver) {
          // draw a rectangle over the coordinate track to handle mouse events

          svgToDraw.gestureReceiver = this.createRawRectangle(0, 0,
                          svgToDraw.getAttributeNS(null, 'width'),
                          svgToDraw.getAttributeNS(null, 'height'),
                          {fill: 'none', class: 'pointerHandler'}, svgToDraw)
          Polymer.dom(svgToDraw).appendChild(svgToDraw.gestureReceiver)
        }
        Polymer.dom.flush()
        this.listen(svgToDraw, 'track', 'dragHandler')
        this.listen(svgToDraw, 'wheel', 'wheelHandler')
      },

      /**
       * changeViewWindowAfterResize - Change window width and height
       *   (__e.g.__ resizing browser window from an `iron-resized` event).
       * Notice that this is a track-level solution: the first track(s) (very
       *   likely to be coordinates) will generate a new `newViewWindow` value
       *   while the rest of the tracks will follow such window
       *
       * @param  {number} newWindowWidth The new window width (in pixels)
       * @param  {number} newViewWindow  The new view window coordinate.
       *   If this value is `undefined` or `null`, then a new one will be
       *   generated based on the current view window, current width and
       *   new width.
       */
      changeViewWindowAfterResize: function (newWindowWidth, newViewWindow) {
        // this is only used to change the viewWindow of mainSvg (both narrow and wide mode)
        if (!newViewWindow) {
          // no new window, need to calculate
          if (!this.mainSvg || !this.mainSvg.viewWindow) {
            return
          }
          newViewWindow = (newViewWindow === false)
            ? this.mainSvg.viewWindow
            : this.mainSvg.viewWindow.getExtension((newWindowWidth - this.windowWidth) / this.windowWidth,
            null, true, this.parent.ref)
        }
        this.updateTrack(newViewWindow)
      },

      /**
       * setWideModeWidthParameters - set width-related parameters in wide mode.
       *
       * @param  {number} newWidth - new width, in px
       * @param  {number} newTxtMargin - new text margin, in px
       * @param  {ChromRegionLiteral} [newViewWindow] - new view window
       *    coordinates. This is useful when view window has changed in one
       *    track and every other track needs to be brought to synchronization.
       *    If omitted, new view window will be recalculated based on new
       *    width (useful for the first track).
       */
      setWideModeWidthParameters: function (
        newWidth, newTxtMargin, newViewWindow
      ) {
        var widthChanged = false
        if (newTxtMargin && newTxtMargin !== this.textMargin) {
          // needs to change txtMargin
          if (newTxtMargin > 0) {
            this.textMargin = newTxtMargin
            if (!this.textSvg) {
              this.initTextSvgAndHolder()
            }
          } else {
            // delete textSvg
            delete this.textMargin
            if (this.textSvg) {
              Polymer.dom(this.trackSvg).removeChild(this.textSvg)
              delete this.textSvg
            }
          }
          widthChanged = true
        }

        if (newWidth !== this.totalWidth || widthChanged) {
          // needs to change totalWidth as well
          var newWindowWidth = newWidth - (newTxtMargin ? newTxtMargin + this.TEXT_MARGIN_GAP : 0)
          this.changeViewWindowAfterResize(newWindowWidth, newViewWindow)
          this.totalWidth = newWidth
          this.windowWidth = this.totalWidth - (newTxtMargin ? newTxtMargin + this.TEXT_MARGIN_GAP : 0)
        }
      },

      /**
       * setWidthParameters - set width-related parameters and switching
       *    between narrow (to be implemented) or wide mode if needed.
       *
       * @param  {number} newWidth - new width, in px
       * @param  {number} newTxtMargin - new text margin, in px
       * @param  {ChromRegionLiteral} [newViewWindow] - new view window
       *    coordinates. This is useful when view window has changed in one
       *    track and every other track needs to be brought to synchronization.
       *    If omitted, new view window will be recalculated based on new
       *    width (useful for the first track).
       */
      setWidthParameters: function (newWidth, newTxtMargin, newWindow) {
        newWidth = (typeof (newWidth) === 'number' && newWidth > 0) ? newWidth : this.totalWidth
        newTxtMargin = (typeof (newTxtMargin) === 'number' && newTxtMargin > 0) ? newTxtMargin : (this.textMargin ? this.textMargin : null)

        if (newWidth - (newTxtMargin ? newTxtMargin + this.TEXT_MARGIN_GAP : 0) < this.MIN_TOTAL_WIDTH) {
          this.switchNarrowMode(true)
          // TODO: complete narrow mode codes
        } else {
          this.setWideModeWidthParameters(newWidth, newTxtMargin, newWindow)
        }
      },

      /**
       * setHeight - set the height of the track
       *
       * @param  {number} newHeight - new height of the track, in px
       */
      setHeight: function (newHeight) {
        if (newHeight > 0) {
          this._setTrackHeight(newHeight)
        } else {
          throw new Error('Invalid height value: ' + newHeight)
        }
      },

      // ****** visibility control ******

      /**
       * changeVisibility - change the visibility level of the track.
       *
       * @param  {number} newValue description
       * @param  {boolean} relative - whether `newValue` is the absolute
       *    visibility level or the relative level.
       *    If `false`, absolute level is used. Please see
       *    `give.TrackObject.StatusEnum` for possible absolute values.
       *    If `true`, then `newValue` will be used to adjust visibility levels
       *    according to `this.allowedVisibilities`. Negative value means
       *    lowering the amount of details shown.
       */
      changeVisibility: function (newValue, relative) {
        if (!relative) {
          // setting the absolute visibility value,
          // if not allowed, the closest one *with more detail* (read: later in this.allowedVisibilities) will be chosen
          if (this.allowedVisibilities.indexOf(newValue) >= 0) {
            this.activeVisibility = newValue
          } else {
            this.allowedVisibilities.every(function (vis) {
              this.activeVisibility = vis
              return vis < newValue
            }, this)
          }
        } else {
          // just move values within this.allowedVisibilities
          var newIndex = this.allowedVisibilities.indexOf(this.activeVisibility) + newValue
          if (newIndex < 0) {
            newIndex = 0
          } else if (newIndex >= this.allowedVisibilities.length) {
            newIndex = this.allowedVisibilities.length - 1
          }
          this.activeVisibility = this.allowedVisibilities[newIndex]
        }
      },

      /**
       * updateAnno - update track properties from JS annotation map object.
       *    Update happens when the track has never been updated, or
       *    `forceUpdate === true`.
       *
       * @param  {object} annoMap - annotation map used to update properties.
       *    If update happens, all properties of `annoMap` will replace
       *    properties with the same name in the track.
       * @param  {boolean} forceUpdate - update annotation even if this track
       *    has already updated before.
       */
      updateAnno: function (annoMap, forceUpdate) {
        if (forceUpdate === true || !this.annoUpdated) {
          for (var key in annoMap) {
            if (key !== 'track' && annoMap.hasOwnProperty(key)) {
              this.parent.setSetting(key, annoMap[key])
            }
          }
        }
        this.annoUpdated = true
      },

      // ****** customized methods ******

      /**
       * drawData - draw the data of the track
       *    This method should be implemented in track implementations.
       *    When implementing this method, use `this._getDataObject` to get
       *    the GiveTree object storing all necessary data corresponding to
       *    the correct chromosome. Then use `.traverse` to traverse through
       *    the content of the GiveTree object and apply customized drawing
       *    methods.
       */
      drawData: function () {
        give._verboseConsole('TrackDOMBehavior.drawData gets called but has' +
          ' not been implemented in `' + this.constructor.name + '`.',
          give.VERBOSE_WARNING)
      },

      getResolution: function (vWindow) {
        return null
      },

      getCurrentViewWindowExt: function (extension) {
        return this.mainSvg.viewWindow.getExtension(extension, null, true, this.parent.ref)
      },

      // ****** common methods used by all tracks ******

      /**
       * _getDataObject - get the data structure from `this.parent`
       *
       * @param  {string} [chrom] - the chromosome to be fetched. If omitted,
       *    use `this.mainSvg.viewWindow.chr`
       * @returns {GiveTreeBase|null} return the data structure object.
       */
      _getDataObject: function (chrom) {
        chrom = chrom || this.mainSvg.viewWindow.chr
        return this.parent.getData(chrom)
      },

      // ** coordinates **

      transformXCoordinate: function (coordinate, moveOutsideToBorder, svgToDraw) {
        // this function is used to transform a given coordinate to svg x
        // TODO: implement chromosomal based coordinates
        // TODO: move this to TrackBehavior behavior
        // coordinate should be {chr: xxx, coor: xxx}
        moveOutsideToBorder = moveOutsideToBorder || false
        svgToDraw = svgToDraw || this.mainSvg
        var windowToDraw = svgToDraw.viewWindow
        if (windowToDraw.chr !== coordinate.chr) {
          return 0
        }
        try {
          var result = (coordinate.coor - windowToDraw.start + 0.5) * this.windowWidth /
            parseFloat(windowToDraw.getLength())
          if (moveOutsideToBorder) {
            if (result < 0) {
              return -1
            } else if (result > this.windowWidth) {
              return this.windowWidth + 1
            }
          }
          return result
        } catch (e) {
          return 0
        }
      },

      revTransXCoordinate: function (x, svgToDraw) {
        // reversely translate X value from JavaScript to chromosome coordinate
        svgToDraw = svgToDraw || this.mainSvg
        var windowToDraw = svgToDraw.viewWindow || Polymer.dom(svgToDraw).parentNode.viewWindow
        return give.ChromRegion.clipCoordinate({chr: windowToDraw.chr,
          coor: parseInt(x / this.windowWidth * windowToDraw.getLength() +
            windowToDraw.start + 0.5)}, this.parent.ref)
      },

      // ** Simple SVG Drawing **

      addElement: function (elem, svgToDraw) {
        svgToDraw = svgToDraw || this.mainSvg
        svgToDraw = svgToDraw.holder || svgToDraw
        Polymer.dom(svgToDraw).appendChild(elem)
      },

      removeElement: function (elem, svgToDraw) {
        svgToDraw = svgToDraw || this.mainSvg
        svgToDraw = svgToDraw.holder || svgToDraw
        Polymer.dom(svgToDraw).removeChild(elem)
      },

      // Create another group on top of the holder group, used to capture gesture events

      clear: function () {
        if (this.textSvg) {
          this.clearSvg(this.textSvg)
        }
        this.clearSvg(this.mainSvg)
      },

      clearSvg: function (svgElem) {
        if (svgElem.holder) {
          try {
            Polymer.dom(svgElem).removeChild(svgElem.holder)
          } catch (err) {
            give._verboseConsole(err, give.VERBOSE_DEBUG_MORE)
          }
          this.initSvgHolder(svgElem)
        } else {
          while (Polymer.dom(svgElem).firstChild) {
            Polymer.dom(svgElem).removeChild(Polymer.dom(svgElem).firstChild)
          }
        }
      },

      updateLocationSize: function (x, y, width, height, newTxtWidth, newWindow) {
        if (typeof (x) === 'number') {
          this.x = x
          // this.trackSvg.setAttributeNS(null, 'x', this.x);
        }
        if (typeof (y) === 'number') {
          this.y = y
          // this.trackSvg.setAttributeNS(null, 'y', this.y);
        }
        if (typeof (width) === 'number') {
          this.setWidthParameters(width, newTxtWidth, newWindow)  // may involve narrowMode
        }
        if (typeof (height) === 'number') {
          this.setHeight(height) // may also involve narrowMode?
        }
        this.setSvgSizeLocation()
      },

      drawLine: function (x1, y1, x2, y2, colorRGB, svgToDraw) {
        // notice that this function draws line according to svg coordinates, not genomic coordinates
        // color is a 24-bit number
        var newLine = document.createElementNS(this.svgNS, 'line')
        newLine.setAttributeNS(null, 'x1', x1)
        newLine.setAttributeNS(null, 'x2', x2)
        newLine.setAttributeNS(null, 'y1', y1)
        newLine.setAttributeNS(null, 'y2', y2)
        if (colorRGB || colorRGB === 0) {
          newLine.setAttributeNS(null, 'stroke', this.rgbToHex(colorRGB))
        }
        this.addElement(newLine, svgToDraw)
        return newLine
      },

      createRawRectangle: function (x1, y1, x2, y2, params, svgToDraw) {
        var xmin = Math.min(x1, x2)
        var ymin = Math.min(y1, y2)
        var newRegion = document.createElementNS(this.svgNS, 'rect')
        newRegion.setAttributeNS(null, 'x', xmin)
        newRegion.setAttributeNS(null, 'y', ymin)
        newRegion.setAttributeNS(null, 'width', Math.abs(x2 - x1))
        newRegion.setAttributeNS(null, 'height', Math.abs(y2 - y1))
        this.setElementParams(newRegion, params)
        this.addElement(newRegion, svgToDraw)
        return newRegion
      },

      createRawPolygon: function (coordinates, params, svgToDraw) {
        // coordinates is an array of string "x,y"
        var newPolygon = document.createElementNS(this.svgNS, 'polygon')
        newPolygon.setAttributeNS(null, 'points', coordinates.join(' '))
        this.setElementParams(newPolygon, params)
        this.addElement(newPolygon, svgToDraw)
        return newPolygon
      },

      createText: function (x, y, text, textAnchor, params) {
        var newLabel = document.createElementNS(this.svgNS, 'text')
        if (textAnchor) {
          newLabel.setAttributeNS(null, 'text-anchor', textAnchor)
        }
        newLabel.setAttributeNS(null, 'x', x)
        newLabel.setAttributeNS(null, 'y', y)
        // TODO: when Edge supports SVG 1.1 dominant-baseline feature, remove this
        if (document.documentMode || navigator.userAgent.match(/Edge/)) {
          newLabel.setAttributeNS(null, 'dy', this.textSize * 0.2)
        }
        // End MSIE & Edge only hack

        this.setElementParams(newLabel, params)
        Polymer.dom(newLabel).appendChild(document.createTextNode(text))
        return newLabel
      },

      drawText: function (x, y, text, textAnchor, params, svgToDraw) {
        var newLabel = this.createText(x, y, text, textAnchor, params)
        this.addElement(newLabel, svgToDraw)
        return newLabel
      },

      testTextWidth: function (text, textAnchor, params, svgToDraw) {
        // this is used to test the length of the text
        var tmpText = this.drawText(0, this.Y_HIDDEN, text,
                    textAnchor, params, svgToDraw)
        Polymer.dom.flush()
        var result = tmpText.getBBox().width
        this.removeElement(tmpText, svgToDraw)
        return result
      },

      setElementParams: function (elem, params) {
        for (var key in params) {
          if (params.hasOwnProperty(key) && params[key] !== undefined && params[key] !== null) {
            if ((key === 'fill' || key === 'stroke') && (typeof params[key] === 'number')) {
              // need to convert integer color to string
              params[key] = this.rgbToHex(params[key])
            }
            elem.setAttributeNS(null, key, params[key])
          }
        }
      },

      // ** Complex SVG drawing for track **

      drawRectangle: function (
        region, colorRGB, y, height, svgToDraw, strokeColorRGB, vertAlign
      ) {
        // height is the percentage of svgToDraw, top-aligned
        // color is an index from colorSet
        // TODO: allow different alignment

        height = height || 1
        y = y || 0

        svgToDraw = svgToDraw || this.mainSvg
        var windowToDraw = svgToDraw.viewWindow

        if (vertAlign === give.TrackDOMBehaviorImpl.VALIGN_CENTER) {
          y -= height / 2
        } else if (vertAlign === give.TrackDOMBehaviorImpl.VALIGN_BOTTOM) {
          y -= height
        }

        if (windowToDraw.overlaps(region) > 0) {
          var x0 = this.transformXCoordinate(region.getStartCoor(), true)
          var x1 = this.transformXCoordinate(region.getEndCoor(), true)
          var newRegion = this.createRawRectangle(x0, y, x1, y + height,
            {fill: colorRGB,
              stroke: (typeof strokeColorRGB === 'number') ? strokeColorRGB : 'none',
              id: region.id}, svgToDraw)

          // add strand info
          this.drawStrandArrows(x0, y, x1, y + height, region.getStrand(), 0xFFFFFF, svgToDraw)
        }
        return newRegion
      },

      drawStrandArrows: function (x0, y0, x1, y1, strand, colorRGB, svgToDraw) {
        if (typeof (strand) === 'string') {
          // first calculate number of arrows
          var arrowWidth = (y1 - y0) * 0.5 * this.ARROW_HEIGHT_PROP
          if (arrowWidth < this.ARROW_MIN_HEIGHT * 0.5) {
            arrowWidth = Math.min(this.ARROW_MIN_HEIGHT, (y1 - y0) * 0.5 * this.ARROW_MAX_HEIGHT_PROP)
          }
          if (x1 - x0 >= arrowWidth * this.ARROW_RECT_MIN_WIDTH) {
            // needs some width to draw arrow
            var arrowCenters = []
            if (x1 - x0 > arrowWidth * (this.ARROW_FLANK_WIDTH * 2 + this.ARROW_GAP_WIDTH + 2)) {
              // multiple arrows
              var numArrows = Math.floor(((x1 - x0) / arrowWidth - this.ARROW_FLANK_WIDTH * 2 - 1) /
                (this.ARROW_GAP_WIDTH + 1)) + 1
              // calculate the center for the first arrow
              var arrowCenter = (x0 + x1) * 0.5 - ((this.ARROW_GAP_WIDTH + 1) * arrowWidth * (numArrows - 1) * 0.5)
              for (var i = 0; i < numArrows; i++) {
                arrowCenters.push(arrowCenter)
                arrowCenter = arrowCenter + (1 + this.ARROW_GAP_WIDTH) * arrowWidth
              }
            } else {
              arrowCenters.push(0.5 * (x1 + x0))
            }

            arrowCenters.forEach(function (xCtr) {
              this.drawSingleStrandArrow(xCtr, (y1 + y0) * 0.5, arrowWidth, strand, colorRGB, svgToDraw)
            }, this)
          }
        }
      },

      drawSingleStrandArrow: function (xCtr, yCtr, width, strand, colorRGB, svgToDraw) {
        var halfWidthX = ((strand === '+') ? width * 0.5 : -width * 0.5)
        this.drawLine(xCtr - halfWidthX, yCtr - width, xCtr + halfWidthX, yCtr, colorRGB, svgToDraw)
        this.drawLine(xCtr - halfWidthX, yCtr + width, xCtr + halfWidthX, yCtr, colorRGB, svgToDraw)
      },

      createWordWrappedText: function (text, textAnchor, params, svgToDraw, maxWidth, maxHeight) {
        // notice that all whitespaces in text will be subsituted by ' ' and consecutive spaces will be collapsed
        // extendDirection ('up' | 'both' | 'down') determine how extra text will be extended
        // when avoidCollision is true, no text will be drawn
        //    if any of the text component is overlapping with existing text elements
        if (!text) {
          return null
        }
        var textArray = text.split(/\s+/)
        var results = []
        var resultWidths = []
        var currHeight = this.textSize * this.fullHeightRatio
        results.push('')
        resultWidths.push(0)

        textArray.every(function (currWord, index) {
          var currWidth, i
          do {
            currWidth = this.testTextWidth((results[results.length - 1].length > 0
                                              ? results[results.length - 1] + ' '
                                              : '') +
                                            currWord, textAnchor, params, svgToDraw)

            if (currWidth > maxWidth) {
              // this line is full
              if (results[results.length - 1].length <= 0) {
                // single super long word, break the word first
                i = 0
                do {
                  i++
                  results[results.length - 1] = currWord.slice(0, -i)
                  currWidth = this.testTextWidth(results[results.length - 1],
                        textAnchor, params, svgToDraw)
                } while (currWidth > maxWidth && i < currWord.length)
                resultWidths[resultWidths.length - 1] = currWidth
                currWord = currWord.slice(-i)
              }

              // then add another line
              if (currHeight + this.textSize * (this.lineGapRatio + this.fullHeightRatio) > maxHeight) {
                // too many lines, use '...', and end processing
                do {
                  results[results.length - 1] = results[results.length - 1].slice(0, -1)
                  currWidth = this.testTextWidth((results[results.length - 1].length > 0
                                                    ? results[results.length - 1] + ' ...'
                                                    : '...'),
                                                  textAnchor, params, svgToDraw)
                } while (currWidth > maxWidth && results[results.length - 1].length > 0)
                results[results.length - 1] = results[results.length - 1].length > 0
                  ? results[results.length - 1] + ' ...'
                  : '...'
                resultWidths[resultWidths.length - 1] = currWidth
                return false
              }
              results.push('')
              resultWidths.push(0)
              currHeight += this.textSize * (this.lineGapRatio + this.fullHeightRatio)
            } else {
              results[results.length - 1] = (results[results.length - 1].length > 0
                                              ? results[results.length - 1] + ' '
                                              : '') +
                                            currWord
              resultWidths[resultWidths.length - 1] = currWidth
              return true
            }
          } while (currWord.length > 0)
        }, this)

        return results
      },

      drawWordWrappedText: function (x, y, textArr, textAnchor, extendDirection, params, svgToDraw, avoidCollision) {
        // calculate the top line y based on number of lines and everything
        var vertParam = 0.5    // this is for default value ('middle')
        switch (extendDirection) {
          case 'up':
            vertParam = 1
            break
          case 'down':
            vertParam = 0
            break
        }

        // then draw everything in textArr
        // get all text elements in the svg in case collision detection is needed
        var textList = Polymer.dom(svgToDraw).querySelectorAll('text')
        var oldTextArray = Array.prototype.slice.call(textList)
        // then store all new labels in newTextArray for collision detection
        var newTextArray = []
        var currY = y - (textArr.length - 1) * vertParam *
          (this.lineGapRatio + this.fullHeightRatio) * this.textSize
        textArr.forEach(function (line, index) {
          newTextArray.push(this.drawText(
            x, currY, line, textAnchor, params, svgToDraw
          ))
          currY += (this.lineGapRatio + this.fullHeightRatio) * this.textSize
        }, this)

        // collision detection, if collision is detected, remove all new labels
        // because there should not be many text elements within the labeling
        // area, a brute-force approach should be enough
        if (avoidCollision && newTextArray.some(function (newNode) {
          var newBox = newNode.getBBox()
          return oldTextArray.some(function (oldNode) {
            var oldBox = oldNode.getBBox()
            return (newBox.x + newBox.width > oldBox.x) &&
              (oldBox.x + oldBox.width > newBox.x) &&
              (newBox.y + newBox.height > oldBox.y) &&
              (oldBox.y + oldBox.height > newBox.y)
          }, this)
        }, this)) {
          // collision found, remove everything from the newTextArray
          newTextArray.forEach(function (textElem) {
            this.removeElement(textElem, svgToDraw)
          }, this)
        }
      },

      createShortLabelArr: function (label, maxX) {
        maxX = maxX || this.textMargin - this.labelMarginRight
        return this.createWordWrappedText(label || this.getTrackSetting('shortLabel'),
                    'end', null, this.textSvg, maxX, this.trackHeight)
      },

      drawShortLabelArr: function (labelArr, maxX) {
        maxX = maxX || this.textMargin - this.labelMarginRight
        this.drawWordWrappedText(maxX, this.trackHeight * 0.5, labelArr,
                    'end', 'both', null, this.textSvg, true)
      },

      // ** Track event handling and functions **

      drawDataFireWrapper: function (newVWindow) {
        // fire track-ready event to its container (to calculate size and do other stuff)
        this.changeViewWindow(newVWindow)
        var shortLabelArr
        if (this.textSvg) {
          shortLabelArr = this.createShortLabelArr()
        }
        this.drawData()
        if (this.textSvg && shortLabelArr) {
          this.drawShortLabelArr(shortLabelArr)
        }
        // draw a rectangle over the coordinate track to handle mouse events

        if (!Polymer.dom(this.getMainSvg()).querySelector('.pointerHandler')) {
          var newRegion = this.createRawRectangle(0, 0, this.windowWidth,
            this.trackHeight, {fill: 'none', class: 'pointerHandler'})
          Polymer.dom(this.getMainSvg()).appendChild(newRegion)
          Polymer.dom.flush()
          this.listen(this.getMainSvg(), 'track', 'dragHandler')
          this.listen(this.getMainSvg(), 'wheel', 'wheelHandler')
        }
        this._setIsReady(true)
        this.fire('track-ready', {ID: this.parent.getID()})
        if (this.callbackAfterDraw) {
          this.callbackAfterDraw()
          delete this.callbackAfterDraw
        }
      },

      drawDataDebounceWrapper: function (newVWindow) {
        // debounce wrapper for drawData()
        this.debounce(this.updateJobName, this.drawDataFireWrapper.bind(
          this, newVWindow), this._drawDebounceInt)
      },

      checkDataAndUpdate: function (newVWindow) {
        // Steps:
        //    * run this.parent.fetchData with drawData (debounced) as callback
        //    * whenever drawData (debounced) is done, run updateCache
        //    Meanwhile, run fetch data (debounced) without visible callback
        //    * Otherwise, run fetch data with drawData (debounced) as callback
        //    * Also, convert all new ranges into arrays first

        if (this.isDebouncerActive(this.cacheUpdateJobName)) {
          this.cancelDebouncer(this.cacheUpdateJobName)
        }

        var newVWArr = Array.isArray(newVWindow) ? newVWindow : [newVWindow]
        newVWArr.forEach(function (range) {
          range.Resolution = this.getResolution(range) || 1
        }, this)

        // this is to set up cache loading functions as call back after draw
        this.callbackAfterDraw = this.debounce.bind(
          this, this.cacheUpdateJobName, this.parent.fetchData.bind(
            this.parent,
            newVWArr.map(function (range) {
              return range.getExtension(
                give.TrackDOMBehaviorImpl.CacheRangeSpanProportion,
                null, true, this.parent.ref
              )
            }, this), null, this.id
          ), this._cacheDebounceInt)
        this.parent.fetchData(
          newVWArr.map(function (range) {
            return range.getExtension(
              give.TrackDOMBehaviorImpl.DefaultRangeSpanProportion,
              null, true, this.parent.ref
            )
          }, this),
          this.drawDataDebounceWrapper.bind(
            this, newVWindow
          ), this.id
        )
      },

      updateTrack: function (viewWindow, index, threshold) {
        // viewWindow is the new viewWindow value of coordinates
        // index is the index of viewWindow (for tracks with multiple viewWindows)
        // if both are omitted, just refresh the track

        viewWindow = this._verifyViewWindow(viewWindow)

        try {
          // Steps:
          // Change view window by calling changeViewWindow()
          //    May clip viewwindow by ref
          this._setIsReady(false)
          // this.oldViewWindow = this.mainSvg.viewWindow;

          // Get data clipped by viewWindow by calling fetchData()
          //    May also include data preparation
          this.checkDataAndUpdate(viewWindow)

          // Update detailed content by calling drawData()
          //    Will be debounced to prevent lagging
        } catch (e) {
          console.log(e.message)
          console.log(e.stack)
  //        if(this.oldViewWindowString) {
  //          this.set('viewWindowString', this.oldViewWindowString);
  //        }
        }
        return viewWindow
      },

      setTrackVisibility: function (visString) {
        this.parent.setVisibility(visString)
      },

      regionInWindow: function (region, svgToDraw) {
        // return true if region overlaps with svgToDraw.viewWindow
        svgToDraw = svgToDraw || this.mainSvg
        return !!region && svgToDraw.viewWindow.overlaps(region) > 0
      },

      regionWindowDirection: function (region, svgToDraw) {
        // return relative direction of the region with svgToDraw.viewWindow
        // null if not on the same chromosome
        // -1 if region is to the left
        // 1 if region is to the right
        // 0 if overlapping
        svgToDraw = svgToDraw || this.mainSvg
        return svgToDraw.viewWindow.chr === region.chr ? (
              svgToDraw.viewWindow.start > region.end ? -1 : (
                svgToDraw.viewWindow.end < region.start ? 1 : 0)
              ) : null
      },

      changeViewWindow: function (viewWindow) {
        // if (typeof (viewWindow) === 'string') {
        //   this.mainSvg.viewWindow = new give.ChromRegion(viewWindow, this.parent.ref)
        // } else {
        //   this.mainSvg.viewWindow = viewWindow.clipRegion(this.parent.ref).clone()
        // }
        // this.set('viewWindowString', newValue);
        this.mainSvg.viewWindow = viewWindow
      },

      _verifyViewWindow: function (viewWindow) {
        viewWindow = viewWindow || this.mainSvg.viewWindow
        if (typeof (viewWindow) === 'string') {
          return new give.ChromRegion(viewWindow, this.parent.ref)
        } else {
          return viewWindow.clipRegion(this.parent.ref).clone()
        }
        // this.set('viewWindowString', newValue);
      },

      rgbToHex: function (colorRGB) {
        return '#' + ((1 << 24) + colorRGB).toString(16).slice(1)
      },

      tokensToDict: function (line) {
        // this function is used to 'dictionarize' the track settings line
        // it will convert lines such as 'track type=wiggle_0 name="fixedStep" description="fixedStep format"'
        // into JavaScript Objects such as
        // { 'track': null, 'type': 'wiggle_0', 'name': 'fixedStep', 'description': 'fixedStep format' }
        var result = {}
        var tokens = line.split(/\s+/g)
        tokens.every(function (token, index) {
          // notice that there should be no '=' in keys, but '=' should be fine in values
          // also '#' will not be treated as comment if appearing within value and quoted
          var components = token.trim().split('=')
          var key = components.shift()
          var value = components.join('=')
          // detect # in key
          if (key.split('#').length > 1) {
            key = key.split('#', 1)
            if (key.length > 0) {
              // there is still key before '#'
              result[key] = ''
            }
            // whatever follows will be comment
            return false
          }
          if ((value.charAt(0) === "'" && value.charAt(value.length - 1) === "'") ||
            (value.charAt(0) === '"' && value.charAt(value.length - 1) === '"')) {
              // remove outside quotes, also ignore '#'
            value = value.substr(1, value.length - 2)
          } else {
            // detect '#'
            if (value.split('#').length > 1) {
              value = value.split('#', 1)
              result[key] = value
              // whatever follows will be comment
              return false
            }
          }
          result[key] = value
          return true
        }, this)
        return result
      },

      // ** Event handlers **

      dragHandler: function (e, detail) {
      // this is used to handle all 'track' events
        switch (detail.state) {
          case 'start':
          // register initial window
            try {
              e.target.classList.add('grabbing')
              this.dragData = {}
              this.dragData.svgNode = this.getMainSvg()
              this.dragData.oldWindow = this.dragData.svgNode.viewWindow.clone()
              this.dragData.ratio = -1 / parseFloat(this.windowWidth)
              window.getSelection().removeAllRanges()
            } catch (err) {
              console.log(err)
            }
            break
          case 'track':
          // update track coordinates on the fly
          // also update window text
            if (this.dragData) {
              this.fire('update-window', {
                windowIndex: this.windowIndex,
                newWindow: this.dragData.oldWindow.getShift(
                        detail.dx * this.dragData.ratio, true,
                        this.parent.ref).regionToString(false)
              })
            // this.setViewWindowString(this.dragData.oldWindow.getShift(
            //  detail.dx * this.dragData.ratio, true, this.parent.ref).regionToString(false));
            }
            break
          case 'end':
          // update window content
            e.target.classList.remove('grabbing')
            Polymer.dom.flush()
            delete (this.dragData)
            window.getSelection().removeAllRanges()
            break
        }
      },

      wheelHandler: function (e, detail) {
        // do nothing here, will do stuff in coor-track
      },

      switchNarrowMode: function () {
        this.windowWidth = this.totalWidth
      }
    }

    give.TrackDOMBehaviorImpl.VALIGN_TOP = 0
    give.TrackDOMBehaviorImpl.VALIGN_CENTER = 1
    give.TrackDOMBehaviorImpl.VALIGN_BOTTOM = 2

    give.TrackDOMBehaviorImpl.CacheRangeSpanProportion = 0.5
    give.TrackDOMBehaviorImpl.DefaultRangeSpanProportion = 0.1  // this is used for smoothing purposes

    /**
     * @polymerBehavior
     */
    give.TrackDOMBehavior = [
      Polymer.IronResizableBehavior,
      give.TrackDOMBehaviorImpl
    ]

    return give
  })(GIVe || {})

</script>
